/*
 Copyright (c) 2005 Poderosa Project, All Rights Reserved.
 This file is a part of the Granados SSH Client Library that is subject to
 the license included in the distributed package.
 You may not use this file except in compliance with the license.

 $Id: SSH2Connection.cs,v 1.2 2005/04/20 08:58:56 okajima Exp $
*/
using System;
using System.Collections;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Net.Sockets;
using System.Text;
using System.Security.Cryptography;

using Granados.PKI;
using Granados.SSHC;
using Granados.Toolkit;

namespace Granados.SSHCV2
{
    public sealed class SSH2Connection : SSHConnection
    {

        //packet count for transmission and reception
        private int _tSequence;

        //MAC for transmission and reception
        private MAC _tMAC;
        private SSH2PacketBuilder _packetBuilder;

        //server info
        private SSH2ConnectionInfo _cInfo;

        private bool _waitingForPortForwardingResponse;

        private KeyExchanger _asyncKeyExchanger;

        public SSH2Connection(SSHConnectionParameter param, ISSHConnectionEventReceiver r, string serverversion, string clientversion) : base(param, r)
        {
            _cInfo = new SSH2ConnectionInfo();
            _cInfo._serverVersionString = serverversion;
            _cInfo._clientVersionString = clientversion;

            _packetBuilder = new SSH2PacketBuilder(new SynchronizedSSH2PacketHandler());
        }
        internal override IByteArrayHandler PacketBuilder
        {
            get
            {
                return _packetBuilder;
            }
        }
        public override SSHConnectionInfo ConnectionInfo
        {
            get
            {
                return _cInfo;
            }
        }

        internal override AuthenticationResult Connect(AbstractSocket s)
        {
            _stream = s;

            KeyExchanger kex = new KeyExchanger(this, null);
            if (!kex.SynchronousKexExchange())
            {
                _stream.Close();
                return AuthenticationResult.Failure;
            }
            //Step3 user authentication
            ServiceRequest("ssh-userauth");
            _authenticationResult = UserAuth();
            return _authenticationResult;
        }


        private void ServiceRequest(string servicename)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_SERVICE_REQUEST);
            wr.Write(servicename);
            TransmitPacket(wr.ToByteArray());

            byte[] response = ReceivePacket().Data;
            SSH2DataReader re = new SSH2DataReader(response);
            PacketType t = re.ReadPacketType();
            if (t != PacketType.SSH_MSG_SERVICE_ACCEPT)
            {
                throw new SSHException("service establishment failed " + t);
            }

            string s = Encoding.ASCII.GetString(re.ReadString());
            if (servicename != s)
                throw new SSHException("protocol error");
        }

        private AuthenticationResult UserAuth()
        {
            string sn = "ssh-connection";

            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_USERAUTH_REQUEST);
            wr.Write(_param.UserName);
            if (_param.AuthenticationType == AuthenticationType.Password)
            {
                //Password authentication
                wr.Write(sn);
                wr.Write("password");
                wr.Write(false);
                wr.Write(_param.Password);
            }
            else if (_param.AuthenticationType == AuthenticationType.KeyboardInteractive)
            {
                wr.Write(sn);
                wr.Write("keyboard-interactive");
                wr.Write(""); //lang
                wr.Write(""); //submethod
            }
            else
            {
                //public key authentication
                SSH2UserAuthKey kp = SSH2UserAuthKey.FromSECSHStyleFile(_param.IdentityFile, _param.Password);
                SSH2DataWriter signsource = new SSH2DataWriter();
                signsource.WriteAsString(_sessionID);
                signsource.WritePacketType(PacketType.SSH_MSG_USERAUTH_REQUEST);
                signsource.Write(_param.UserName);
                signsource.Write(sn);
                signsource.Write("publickey");
                signsource.Write(true);
                signsource.Write(SSH2Util.PublicKeyAlgorithmName(kp.Algorithm));
                signsource.WriteAsString(kp.GetPublicKeyBlob());

                SSH2DataWriter signpack = new SSH2DataWriter();
                signpack.Write(SSH2Util.PublicKeyAlgorithmName(kp.Algorithm));
                signpack.WriteAsString(kp.Sign(signsource.ToByteArray()));

                wr.Write(sn);
                wr.Write("publickey");
                wr.Write(true);
                wr.Write(SSH2Util.PublicKeyAlgorithmName(kp.Algorithm));
                wr.WriteAsString(kp.GetPublicKeyBlob());
                wr.WriteAsString(signpack.ToByteArray());
            }
            TransmitPacket(wr.ToByteArray());

            _authenticationResult = ProcessAuthenticationResponse();
            if (_authenticationResult == AuthenticationResult.Failure)
                throw new SSHException(Strings.GetString("AuthenticationFailed"));
            return _authenticationResult;
        }
        private AuthenticationResult ProcessAuthenticationResponse()
        {
            do
            {
                SSH2DataReader response = new SSH2DataReader(ReceivePacket().Data);
                PacketType h = response.ReadPacketType();
                if (h == PacketType.SSH_MSG_USERAUTH_FAILURE)
                {
                    string msg = Encoding.ASCII.GetString(response.ReadString());
                    return AuthenticationResult.Failure;
                }
                else if (h == PacketType.SSH_MSG_USERAUTH_BANNER)
                {
                    Debug.WriteLine("USERAUTH_BANNER");
                }
                else if (h == PacketType.SSH_MSG_USERAUTH_SUCCESS)
                {
                    _packetBuilder.Handler = new CallbackSSH2PacketHandler(this);
                    return AuthenticationResult.Success; //successfully exit
                }
                else if (h == PacketType.SSH_MSG_USERAUTH_INFO_REQUEST)
                {
                    string name = Encoding.ASCII.GetString(response.ReadString());
                    string inst = Encoding.ASCII.GetString(response.ReadString());
                    string lang = Encoding.ASCII.GetString(response.ReadString());
                    int num = response.ReadInt32();
                    string[] prompts = new string[num];
                    for (int i = 0; i < num; i++)
                    {
                        prompts[i] = Encoding.ASCII.GetString(response.ReadString());
                        bool echo = response.ReadBool();
                    }
                    _eventReceiver.OnAuthenticationPrompt(prompts);
                    return AuthenticationResult.Prompt;
                }
                else
                    throw new SSHException("protocol error: unexpected packet type " + h);
            } while (true);
        }
        public AuthenticationResult DoKeyboardInteractiveAuth(string[] input)
        {
            if (_param.AuthenticationType != AuthenticationType.KeyboardInteractive)
                throw new SSHException("DoKeyboardInteractiveAuth() must be called with keyboard-interactive authentication");
            SSH2DataWriter re = new SSH2DataWriter();
            re.WritePacketType(PacketType.SSH_MSG_USERAUTH_INFO_RESPONSE);
            re.Write(input.Length);
            foreach (string t in input)
                re.Write(t);
            TransmitPacket(re.ToByteArray());

            _authenticationResult = ProcessAuthenticationResponse();
            //try again on failure
            if (_authenticationResult == AuthenticationResult.Failure)
            {
                SSH2DataWriter wr = new SSH2DataWriter();
                wr.WritePacketType(PacketType.SSH_MSG_USERAUTH_REQUEST);
                wr.Write(_param.UserName);
                wr.Write("ssh-connection");
                wr.Write("keyboard-interactive");
                wr.Write(""); //lang
                wr.Write(""); //submethod
                TransmitPacket(wr.ToByteArray());
                _authenticationResult = ProcessAuthenticationResponse();
            }
            return _authenticationResult;
        }

        public override SSHChannel OpenShell(ISSHChannelEventReceiver receiver)
        {
            //open channel
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_OPEN);
            wr.Write("session");
            int local_channel = this.RegisterChannelEventReceiver(null, receiver)._localID;

            wr.Write(local_channel);
            wr.Write(_param.WindowSize); //initial window size
            int windowsize = _param.WindowSize;
            wr.Write(_param.MaxPacketSize); //max packet size
            SSH2Channel channel = new SSH2Channel(this, ChannelType.Shell, local_channel);
            TransmitPacket(wr.ToByteArray());

            return channel;
        }

        public override SSHChannel ForwardPort(ISSHChannelEventReceiver receiver, string remote_host, int remote_port, string originator_host, int originator_port)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_OPEN);
            wr.Write("direct-tcpip");
            int local_id = RegisterChannelEventReceiver(null, receiver)._localID;
            wr.Write(local_id);
            wr.Write(_param.WindowSize); //initial window size
            int windowsize = _param.WindowSize;
            wr.Write(_param.MaxPacketSize); //max packet size
            wr.Write(remote_host);
            wr.Write(remote_port);
            wr.Write(originator_host);
            wr.Write(originator_port);

            SSH2Channel channel = new SSH2Channel(this, ChannelType.ForwardedLocalToRemote, local_id);

            TransmitPacket(wr.ToByteArray());

            return channel;
        }

        public override void ListenForwardedPort(string allowed_host, int bind_port)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_GLOBAL_REQUEST);
            wr.Write("tcpip-forward");
            wr.Write(true);
            wr.Write(allowed_host);
            wr.Write(bind_port);

            _waitingForPortForwardingResponse = true;
            TransmitPacket(wr.ToByteArray());
        }

        public override void CancelForwardedPort(string host, int port)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_GLOBAL_REQUEST);
            wr.Write("cancel-tcpip-forward");
            wr.Write(true);
            wr.Write(host);
            wr.Write(port);
            TransmitPacket(wr.ToByteArray());
        }

        private void ProcessPortforwardingRequest(ISSHConnectionEventReceiver receiver, SSH2DataReader reader)
        {
            string method = Encoding.ASCII.GetString(reader.ReadString());

            int remote_channel = reader.ReadInt32();
            int window_size = reader.ReadInt32(); //skip initial window size
            int servermaxpacketsize = reader.ReadInt32();
            string host = Encoding.ASCII.GetString(reader.ReadString());
            int port = reader.ReadInt32();
            string originator_ip = Encoding.ASCII.GetString(reader.ReadString());
            int originator_port = reader.ReadInt32();

            PortForwardingCheckResult r = receiver.CheckPortForwardingRequest(host, port, originator_ip, originator_port);
            SSH2DataWriter wr = new SSH2DataWriter();
            if (r.allowed)
            {
                //send OPEN_CONFIRMATION
                SSH2Channel channel = new SSH2Channel(this, ChannelType.ForwardedRemoteToLocal, RegisterChannelEventReceiver(null, r.channel)._localID, remote_channel, servermaxpacketsize);
                wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_OPEN_CONFIRMATION);
                wr.Write(remote_channel);
                wr.Write(channel.LocalChannelID);
                wr.Write(_param.WindowSize); //initial window size
                wr.Write(_param.MaxPacketSize); //max packet size
                receiver.EstablishPortforwarding(r.channel, channel);
            }
            else
            {
                wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_OPEN_FAILURE);
                wr.Write(remote_channel);
                wr.Write(r.reason_code);
                wr.Write(r.reason_message);
                wr.Write(""); //lang tag
            }
            TransmitPacket(wr.ToByteArray());
        }

        internal SSH2Packet TransmitPacket(byte[] payload)
        {
            lock (_tLockObject)
            {
                SSH2Packet p = SSH2Packet.FromPlainPayload(payload, _tCipher == null ? 8 : _tCipher.BlockSize, _param.Random);
                if (_tMAC != null) p.CalcHash(_tMAC, _tSequence);

                _tSequence++;
                p.WriteTo(_stream, _tCipher);
                return p;
            }
        }

        //synchronous reception
        internal SSH2Packet ReceivePacket()
        {

            while (true)
            {
                SSH2Packet p = null;
                SynchronizedSSH2PacketHandler handler = (SynchronizedSSH2PacketHandler)_packetBuilder.Handler;
                if (!handler.HasPacket)
                {
                    handler.Wait();
                    if (handler.State == ReceiverState.Error)
                        throw new SSHException(handler.ErrorMessage);
                    else if (handler.State == ReceiverState.Closed)
                        throw new SSHException("socket closed");
                }
                p = handler.PopPacket();

                SSH2DataReader r = new SSH2DataReader(p.Data);
                PacketType pt = r.ReadPacketType();
                if (pt == PacketType.SSH_MSG_IGNORE)
                {
                    if (_eventReceiver != null) _eventReceiver.OnIgnoreMessage(r.ReadString());
                }
                else if (pt == PacketType.SSH_MSG_DEBUG)
                {
                    bool f = r.ReadBool();
                    if (_eventReceiver != null) _eventReceiver.OnDebugMessage(f, r.ReadString());
                }
                else
                    return p;
            }
        }
        internal void AsyncReceivePacket(SSH2Packet packet)
        {
            try
            {
                ProcessPacket(packet);
            }
            catch (Exception ex)
            {
                //Debug.WriteLine(ex.StackTrace);
                if (!_closed)
                    _eventReceiver.OnError(ex, ex.Message);
            }
        }

        private bool ProcessPacket(SSH2Packet packet)
        {
            SSH2DataReader r = new SSH2DataReader(packet.Data);
            PacketType pt = r.ReadPacketType();
            //Debug.WriteLine("ProcessPacket pt="+pt);

            if (pt == PacketType.SSH_MSG_DISCONNECT)
            {
                int errorcode = r.ReadInt32();
                //string description = Encoding.ASCII.GetString(r.ReadString());
                _eventReceiver.OnConnectionClosed();
                return false;
            }
            else if (_waitingForPortForwardingResponse)
            {
                if (pt != PacketType.SSH_MSG_REQUEST_SUCCESS)
                    _eventReceiver.OnUnknownMessage((byte)pt, r.Image);
                _waitingForPortForwardingResponse = false;
                return true;
            }
            else if (pt == PacketType.SSH_MSG_CHANNEL_OPEN)
            {
                ProcessPortforwardingRequest(_eventReceiver, r);
                return true;
            }
            else if (pt >= PacketType.SSH_MSG_CHANNEL_OPEN_CONFIRMATION && pt <= PacketType.SSH_MSG_CHANNEL_FAILURE)
            {
                int local_channel = r.ReadInt32();
                ChannelEntry e = FindChannelEntry(local_channel);
                if (e != null) //throw new SSHException("Unknown channel "+local_channel);
                    ((SSH2Channel)e._channel).ProcessPacket(e._receiver, pt, 5 + r.Rest, r);
                else
                    Debug.WriteLine("unexpected channel pt=" + pt + " local_channel=" + local_channel.ToString());
                return true;
            }
            else if (pt == PacketType.SSH_MSG_IGNORE)
            {
                _eventReceiver.OnIgnoreMessage(r.ReadString());
                return true;
            }
            else if (_asyncKeyExchanger != null)
            {
                _asyncKeyExchanger.AsyncProcessPacket(packet);
                return true;
            }
            else if (pt == PacketType.SSH_MSG_KEXINIT)
            {
                //Debug.WriteLine("Host sent KEXINIT");
                _asyncKeyExchanger = new KeyExchanger(this, _sessionID);
                _asyncKeyExchanger.AsyncProcessPacket(packet);
                return true;
            }
            else
            {
                _eventReceiver.OnUnknownMessage((byte)pt, r.Image);
                return false;
            }
        }

        public override void Disconnect(string msg)
        {
            if (_closed) return;
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_DISCONNECT);
            wr.Write(0);
            wr.Write(msg);
            wr.Write(""); //language

            try
            {
                TransmitPacket(wr.ToByteArray());
                _stream.Flush();
                _closed = true;
                _stream.Close();
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode != 10054)
                {
                    throw;
                }
            }
        }

        public override void Close()
        {
            if (_closed) return;
            _closed = true;

            try
            {
                _stream.Close();
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode != 10054)
                {
                    throw;
                }
            }
        }
        public override void SendIgnorableData(string msg)
        {
            SSH2DataWriter w = new SSH2DataWriter();
            w.WritePacketType(PacketType.SSH_MSG_IGNORE);
            w.Write(msg);
            TransmitPacket(w.ToByteArray());
        }
        public void ReexchangeKeys()
        {
            _asyncKeyExchanger = new KeyExchanger(this, _sessionID);
            _asyncKeyExchanger.AsyncStartReexchange();
        }

        internal void LockCommunication()
        {
            _packetBuilder.SetSignal(false);
        }
        internal void UnlockCommunication()
        {
            _packetBuilder.SetSignal(true);
        }
        internal void RefreshKeys(byte[] sessionID, Cipher tc, Cipher rc, MAC tm, MAC rm)
        {
            _sessionID = sessionID;
            _tCipher = tc;
            _tMAC = tm;
            _packetBuilder.SetCipher(rc, _param.CheckMACError ? rm : null);
            _asyncKeyExchanger = null;
        }

    }

    public class SSH2Channel : SSHChannel
    {
        //channel property
        protected int _windowSize;
        protected int _leftWindowSize;
        protected int _serverMaxPacketSize;
        protected int _allowedDataSize;

        //negotiation status
        protected int _negotiationStatus;

        public SSH2Channel(SSHConnection con, ChannelType type, int local_id) : base(con, type, local_id)
        {
            _windowSize = _leftWindowSize = con.Param.WindowSize;
            _negotiationStatus = type == ChannelType.Shell ? 3 : type == ChannelType.ForwardedLocalToRemote ? 1 : type == ChannelType.Session ? 1 : 0;
        }
        public SSH2Channel(SSHConnection con, ChannelType type, int local_id, int remote_id, int maxpacketsize) : base(con, type, local_id)
        {
            _windowSize = _leftWindowSize = con.Param.WindowSize;
            Debug.Assert(type == ChannelType.ForwardedRemoteToLocal);
            _remoteID = remote_id;
            _serverMaxPacketSize = maxpacketsize;
            _negotiationStatus = 0;
        }

        public override void ResizeTerminal(int width, int height, int pixel_width, int pixel_height)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_REQUEST);
            wr.Write(_remoteID);
            wr.Write("window-change");
            wr.Write(false);
            wr.Write(width);
            wr.Write(height);
            wr.Write(pixel_width); //no graphics
            wr.Write(pixel_height);
            TransmitPacket(wr.ToByteArray());
        }
        public override void Transmit(byte[] data)
        {
            //!!it is better idea that we wait a WINDOW_ADJUST if the left size is lack
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_DATA);
            wr.Write(_remoteID);
            wr.WriteAsString(data);

            TransmitPacket(wr.ToByteArray());
        }
        public override void Transmit(byte[] data, int offset, int length)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_DATA);
            wr.Write(_remoteID);
            wr.WriteAsString(data, offset, length);

            TransmitPacket(wr.ToByteArray());
        }

        public override void SendEOF()
        {
            if (_connection.IsClosed) return;
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_EOF);
            wr.Write(_remoteID);
            TransmitPacket(wr.ToByteArray());
        }


        public override void Close()
        {
            if (_connection.IsClosed) return;
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_CLOSE);
            wr.Write(_remoteID);

            try
            {
                TransmitPacket(wr.ToByteArray());
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode != 10054)
                {
                    throw;
                }
            }
        }

        //maybe this is SSH2 only feature
        public void SetEnvironmentVariable(string name, string value)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_REQUEST);
            wr.Write(_remoteID);
            wr.Write("env");
            wr.Write(false);
            wr.Write(name);
            wr.Write(value);
            TransmitPacket(wr.ToByteArray());
        }
        public void SendBreak(int time)
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_REQUEST);
            wr.Write(_remoteID);
            wr.Write("break");
            wr.Write(true);
            wr.Write(time);
            TransmitPacket(wr.ToByteArray());
        }

        internal void ProcessPacket(ISSHChannelEventReceiver receiver, PacketType pt, int data_length, SSH2DataReader re)
        {
            //NOTE: the offset of 're' is next to 'receipiant channel' field
            _leftWindowSize -= data_length;
            if (pt != PacketType.SSH_MSG_CHANNEL_EOF && pt != PacketType.SSH_MSG_CHANNEL_CLOSE)
            { //window adjust is not necessary if the channel is being closed
                while (_leftWindowSize <= _windowSize)
                {
                    SSH2DataWriter adj = new SSH2DataWriter();
                    adj.WritePacketType(PacketType.SSH_MSG_CHANNEL_WINDOW_ADJUST);
                    adj.Write(_remoteID);
                    adj.Write(_windowSize);
                    TransmitPacket(adj.ToByteArray());
                    _leftWindowSize += _windowSize;
                    //Debug.WriteLine("Window size is adjusted to " + _leftWindowSize);
                }
            }

            if (pt == PacketType.SSH_MSG_CHANNEL_WINDOW_ADJUST)
            {
                int w = re.ReadInt32();
                //some servers may not send SSH_MSG_CHANNEL_WINDOW_ADJUST. 
                //it is dangerous to wait this message in send procedure
                _allowedDataSize += w;
                //Debug.WriteLine(String.Format("Window Adjust +={0}",w));
            }
            else if (_negotiationStatus != 0)
            { //when the negotiation is not completed
                if (_type == ChannelType.Shell)
                    OpenShell(receiver, pt, re);
                else if (_type == ChannelType.ForwardedLocalToRemote)
                    ReceivePortForwardingResponse(receiver, pt, re);
                else if (_type == ChannelType.Session)
                    EstablishSession(receiver, pt, re);
            }
            else
            {
                switch (pt)
                {
                    case PacketType.SSH_MSG_CHANNEL_DATA:
                        {
                            int len = re.ReadInt32();
                            receiver.OnData(re.Image, re.Offset, len);
                        }
                        break;
                    case PacketType.SSH_MSG_CHANNEL_EXTENDED_DATA:
                        {
                            int t = re.ReadInt32();
                            byte[] data = re.ReadString();
                            receiver.OnExtendedData(t, data);
                        }
                        break;
                    case PacketType.SSH_MSG_CHANNEL_REQUEST:
                        {
                            string request = Encoding.ASCII.GetString(re.ReadString());
                            bool reply = re.ReadBool();
                            if (request == "exit-status")
                            {
                                int status = re.ReadInt32();
                            }
                            else if (reply)
                            { //we reject unknown requests including keep-alive check
                                SSH2DataWriter wr = new SSH2DataWriter();
                                wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_FAILURE);
                                wr.Write(_remoteID);
                                TransmitPacket(wr.ToByteArray());
                            }
                        }
                        break;
                    case PacketType.SSH_MSG_CHANNEL_EOF:
                        receiver.OnChannelEOF();
                        break;
                    case PacketType.SSH_MSG_CHANNEL_CLOSE:
                        _connection.UnregisterChannelEventReceiver(_localID);
                        receiver.OnChannelClosed();
                        break;
                    case PacketType.SSH_MSG_CHANNEL_FAILURE:
                    case PacketType.SSH_MSG_CHANNEL_SUCCESS:
                        receiver.OnMiscPacket((byte)pt, re.Image, re.Offset, re.Rest);
                        break;
                    default:
                        receiver.OnMiscPacket((byte)pt, re.Image, re.Offset, re.Rest);
                        Debug.WriteLine("Unknown Packet " + pt);
                        break;
                }

            }

        }

        private void TransmitPacket(byte[] data)
        {
            _allowedDataSize -= data.Length;
            ((SSH2Connection)_connection).TransmitPacket(data);
        }

        private void OpenShell(ISSHChannelEventReceiver receiver, PacketType pt, SSH2DataReader reader)
        {
            if (_negotiationStatus == 3)
            {
                if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_CONFIRMATION)
                {
                    if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_FAILURE)
                        receiver.OnChannelError(null, "opening channel failed; packet type=" + pt);
                    else
                    {
                        int errcode = reader.ReadInt32();
                        string msg = Encoding.ASCII.GetString(reader.ReadString());
                        receiver.OnChannelError(null, msg);
                    }
                    Close();
                }
                else
                {
                    _remoteID = reader.ReadInt32();
                    _allowedDataSize = reader.ReadInt32();
                    _serverMaxPacketSize = reader.ReadInt32();

                    //open pty
                    SSH2DataWriter wr = new SSH2DataWriter();
                    wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_REQUEST);
                    wr.Write(_remoteID);
                    wr.Write("pty-req");
                    wr.Write(true);
                    wr.Write(_connection.Param.TerminalName);
                    wr.Write(_connection.Param.TerminalWidth);
                    wr.Write(_connection.Param.TerminalHeight);
                    wr.Write(_connection.Param.TerminalPixelWidth);
                    wr.Write(_connection.Param.TerminalPixelHeight);
                    wr.WriteAsString(new byte[0]);
                    TransmitPacket(wr.ToByteArray());

                    _negotiationStatus = 2;
                }
            }
            else if (_negotiationStatus == 2)
            {
                if (pt != PacketType.SSH_MSG_CHANNEL_SUCCESS)
                {
                    receiver.OnChannelError(null, "opening pty failed");
                    Close();
                }
                else
                {
                    //open shell
                    SSH2DataWriter wr = new SSH2DataWriter();
                    wr.WritePacketType(PacketType.SSH_MSG_CHANNEL_REQUEST);
                    wr.Write(_remoteID);
                    wr.Write("shell");
                    wr.Write(true);
                    TransmitPacket(wr.ToByteArray());

                    _negotiationStatus = 1;
                }
            }
            else if (_negotiationStatus == 1)
            {
                if (pt != PacketType.SSH_MSG_CHANNEL_SUCCESS)
                {
                    receiver.OnChannelError(null, "Opening shell failed: packet type=" + pt.ToString());
                    Close();
                }
                else
                {
                    receiver.OnChannelReady();
                    _negotiationStatus = 0; //goal!
                }
            }
            else
                Debug.Assert(false);
        }

        private void ReceivePortForwardingResponse(ISSHChannelEventReceiver receiver, PacketType pt, SSH2DataReader reader)
        {
            if (_negotiationStatus == 1)
            {
                if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_CONFIRMATION)
                {
                    if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_FAILURE)
                        receiver.OnChannelError(null, "opening channel failed; packet type=" + pt);
                    else
                    {
                        int errcode = reader.ReadInt32();
                        string msg = Encoding.ASCII.GetString(reader.ReadString());
                        receiver.OnChannelError(null, msg);
                    }
                    Close();
                }
                else
                {
                    _remoteID = reader.ReadInt32();
                    _serverMaxPacketSize = reader.ReadInt32();
                    _negotiationStatus = 0;
                    receiver.OnChannelReady();
                }
            }
            else
                Debug.Assert(false);
        }
        private void EstablishSession(ISSHChannelEventReceiver receiver, PacketType pt, SSH2DataReader reader)
        {
            if (_negotiationStatus == 1)
            {
                if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_CONFIRMATION)
                {
                    if (pt != PacketType.SSH_MSG_CHANNEL_OPEN_FAILURE)
                        receiver.OnChannelError(null, "opening channel failed; packet type=" + pt);
                    else
                    {
                        int remote_id = reader.ReadInt32();
                        int errcode = reader.ReadInt32();
                        string msg = Encoding.ASCII.GetString(reader.ReadString());
                        receiver.OnChannelError(null, msg);
                    }
                    Close();
                }
                else
                {
                    _remoteID = reader.ReadInt32();
                    _serverMaxPacketSize = reader.ReadInt32();
                    _negotiationStatus = 0;
                    receiver.OnChannelReady();
                }
            }
            else
                Debug.Assert(false);
        }
    }

    internal class KeyExchanger
    {
        private SSH2Connection _con;
        private SSHConnectionParameter _param;
        private SSH2ConnectionInfo _cInfo;
        //payload of KEXINIT message
        private byte[] _serverKEXINITPayload;
        private byte[] _clientKEXINITPayload;

        //true if the host sent KEXINIT first
        private bool _startedByHost;

        private ManualResetEvent _newKeyEvent;

        //status
        private enum Status
        {
            INITIAL,
            WAIT_KEXINIT,
            WAIT_KEXDH_REPLY,
            WAIT_NEWKEYS,
            FINISHED
        }
        private Status _status;

        private BigInteger _x;
        private BigInteger _e;
        private BigInteger _k;
        private byte[] _hash;
        private byte[] _sessionID;

        //results
        Cipher _rc;
        Cipher _tc;
        MAC _rm;
        MAC _tm;

        private void TransmitPacket(byte[] payload)
        {
            _con.TransmitPacket(payload);
        }

        public KeyExchanger(SSH2Connection con, byte[] sessionID)
        {
            _con = con;
            _param = con.Param;
            _cInfo = (SSH2ConnectionInfo)con.ConnectionInfo;
            _sessionID = sessionID;
            _status = Status.INITIAL;
        }

        public bool SynchronousKexExchange()
        {
            SendKEXINIT();
            ProcessKEXINIT(_con.ReceivePacket());
            SendKEXDHINIT();
            if (!ProcessKEXDHREPLY(_con.ReceivePacket())) return false;
            SendNEWKEYS();
            ProcessNEWKEYS(_con.ReceivePacket());
            return true;
        }
        public void AsyncStartReexchange()
        {
            _startedByHost = false;
            _status = Status.WAIT_KEXINIT;
            SendKEXINIT();
        }
        public void AsyncProcessPacket(SSH2Packet packet)
        {
            switch (_status)
            {
                case Status.INITIAL:
                    _startedByHost = true;
                    ProcessKEXINIT(packet);
                    SendKEXINIT();
                    SendKEXDHINIT();
                    break;
                case Status.WAIT_KEXINIT:
                    ProcessKEXINIT(packet);
                    SendKEXDHINIT();
                    break;
                case Status.WAIT_KEXDH_REPLY:
                    ProcessKEXDHREPLY(packet);
                    SendNEWKEYS();
                    break;
                case Status.WAIT_NEWKEYS:
                    ProcessNEWKEYS(packet);
                    Debug.Assert(_status == Status.FINISHED);
                    break;
            }
        }

        private void SendKEXINIT()
        {
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_KEXINIT);
            byte[] cookie = new byte[16];
            _param.Random.NextBytes(cookie);
            wr.Write(cookie);
            wr.Write("diffie-hellman-group1-sha1"); //    kex_algorithms
            wr.Write(FormatHostKeyAlgorithmDescription());            //    server_host_key_algorithms
            wr.Write(FormatCipherAlgorithmDescription());      //    encryption_algorithms_client_to_server
            wr.Write(FormatCipherAlgorithmDescription());      //    encryption_algorithms_server_to_client
            wr.Write("hmac-sha1");                  //    mac_algorithms_client_to_server
            wr.Write("hmac-sha1");                  //    mac_algorithms_server_to_client
            wr.Write("none");                       //    compression_algorithms_client_to_server
            wr.Write("none");                       //    compression_algorithms_server_to_client
            wr.Write("");                           //    languages_client_to_server
            wr.Write("");                           //    languages_server_to_client
            wr.Write(false); //Indicates whether a guessed key exchange packet follows
            wr.Write(0);       //reserved for future extension

            _clientKEXINITPayload = wr.ToByteArray();
            _status = Status.WAIT_KEXINIT;
            TransmitPacket(_clientKEXINITPayload);
        }
        private void ProcessKEXINIT(SSH2Packet packet)
        {
            _serverKEXINITPayload = packet.Data;
            SSH2DataReader re = new SSH2DataReader(_serverKEXINITPayload);
            byte[] head = re.Read(17); //Type and cookie
            if (head[0] != (byte)PacketType.SSH_MSG_KEXINIT) throw new SSHException(String.Format("Server response is not SSH_MSG_KEXINIT but {0}", head[0]));
            Encoding enc = Encoding.ASCII;

            string kex = enc.GetString(re.ReadString());
            _cInfo._supportedKEXAlgorithms = kex;
            CheckAlgorithmSupport("keyexchange", kex, "diffie-hellman-group1-sha1");

            string host_key = enc.GetString(re.ReadString());
            _cInfo._supportedHostKeyAlgorithms = host_key;
            _cInfo._algorithmForHostKeyVerification = DecideHostKeyAlgorithm(host_key);

            string enc_cs = enc.GetString(re.ReadString());
            _cInfo._supportedCipherAlgorithms = enc_cs;
            _cInfo._algorithmForTransmittion = DecideCipherAlgorithm(enc_cs);

            string enc_sc = enc.GetString(re.ReadString());
            _cInfo._algorithmForReception = DecideCipherAlgorithm(enc_sc);

            string mac_cs = enc.GetString(re.ReadString());
            CheckAlgorithmSupport("mac", mac_cs, "hmac-sha1");

            string mac_sc = enc.GetString(re.ReadString());
            CheckAlgorithmSupport("mac", mac_sc, "hmac-sha1");

            string comp_cs = enc.GetString(re.ReadString());
            CheckAlgorithmSupport("compression", comp_cs, "none");
            string comp_sc = enc.GetString(re.ReadString());
            CheckAlgorithmSupport("compression", comp_sc, "none");

            string lang_cs = enc.GetString(re.ReadString());
            string lang_sc = enc.GetString(re.ReadString());
            bool flag = re.ReadBool();
            int reserved = re.ReadInt32();
            Debug.Assert(re.Rest == 0);
            if (flag) throw new SSHException("Algorithm negotiation failed");
        }


        private void SendKEXDHINIT()
        {
            //Round1 computes and sends [e]
            byte[] sx = new byte[16];
            _param.Random.NextBytes(sx);
            _x = new BigInteger(sx);
            _e = new BigInteger(2).modPow(_x, DH_PRIME);
            SSH2DataWriter wr = new SSH2DataWriter();
            wr.WritePacketType(PacketType.SSH_MSG_KEXDH_INIT);
            wr.Write(_e);
            _status = Status.WAIT_KEXDH_REPLY;
            TransmitPacket(wr.ToByteArray());
        }
        private bool ProcessKEXDHREPLY(SSH2Packet packet)
        {
            //Round2 receives response
            SSH2DataReader re = new SSH2DataReader(packet.Data);
            PacketType h = re.ReadPacketType();
            if (h != PacketType.SSH_MSG_KEXDH_REPLY) throw new SSHException(String.Format("KeyExchange response is not KEXDH_REPLY but {0}", h));
            byte[] key_and_cert = re.ReadString();
            BigInteger f = re.ReadMPInt();
            byte[] signature = re.ReadString();
            Debug.Assert(re.Rest == 0);

            //Round3 calc hash H
            SSH2DataWriter wr = new SSH2DataWriter();
            _k = f.modPow(_x, DH_PRIME);
            wr = new SSH2DataWriter();
            wr.Write(_cInfo._clientVersionString);
            wr.Write(_cInfo._serverVersionString);
            wr.WriteAsString(_clientKEXINITPayload);
            wr.WriteAsString(_serverKEXINITPayload);
            wr.WriteAsString(key_and_cert);
            wr.Write(_e);
            wr.Write(f);
            wr.Write(_k);
            _hash = new SHA1CryptoServiceProvider().ComputeHash(wr.ToByteArray());

            if (!VerifyHostKey(key_and_cert, signature, _hash)) return false;

            //Debug.WriteLine("hash="+DebugUtil.DumpByteArray(hash));
            if (_sessionID == null) _sessionID = _hash;
            return true;
        }
        private void SendNEWKEYS()
        {
            _status = Status.WAIT_NEWKEYS;
            _newKeyEvent = new ManualResetEvent(false);
            TransmitPacket(new byte[1] { (byte)PacketType.SSH_MSG_NEWKEYS });

            //establish Ciphers
            _tc = CipherFactory.CreateCipher(SSHProtocol.SSH2, _cInfo._algorithmForTransmittion,
                DeriveKey(_k, _hash, 'C', CipherFactory.GetKeySize(_cInfo._algorithmForTransmittion)), DeriveKey(_k, _hash, 'A', CipherFactory.GetBlockSize(_cInfo._algorithmForTransmittion)));
            _rc = CipherFactory.CreateCipher(SSHProtocol.SSH2, _cInfo._algorithmForReception,
                DeriveKey(_k, _hash, 'D', CipherFactory.GetKeySize(_cInfo._algorithmForReception)), DeriveKey(_k, _hash, 'B', CipherFactory.GetBlockSize(_cInfo._algorithmForReception)));

            //establish MACs
            MACAlgorithm ma = MACAlgorithm.HMACSHA1;
            _tm = MACFactory.CreateMAC(MACAlgorithm.HMACSHA1, DeriveKey(_k, _hash, 'E', MACFactory.GetSize(ma)));
            _rm = MACFactory.CreateMAC(MACAlgorithm.HMACSHA1, DeriveKey(_k, _hash, 'F', MACFactory.GetSize(ma)));
            _newKeyEvent.Set();
        }
        private void ProcessNEWKEYS(SSH2Packet packet)
        {
            //confirms new key
            try
            {
                byte[] response = packet.Data;
                if (response.Length != 1 || response[0] != (byte)PacketType.SSH_MSG_NEWKEYS) throw new SSHException("SSH_MSG_NEWKEYS failed");

                _newKeyEvent.WaitOne();
                _newKeyEvent.Close();
                _con.LockCommunication();
                _con.RefreshKeys(_sessionID, _tc, _rc, _tm, _rm);
                _status = Status.FINISHED;
            }
            finally
            {
                _con.UnlockCommunication();
            }
        }

        private bool VerifyHostKey(byte[] K_S, byte[] signature, byte[] hash)
        {
            SSH2DataReader re1 = new SSH2DataReader(K_S);
            string algorithm = Encoding.ASCII.GetString(re1.ReadString());
            if (algorithm != SSH2Util.PublicKeyAlgorithmName(_cInfo._algorithmForHostKeyVerification))
                throw new SSHException("Protocol Error: Host Key Algorithm Mismatch");

            SSH2DataReader re2 = new SSH2DataReader(signature);
            algorithm = Encoding.ASCII.GetString(re2.ReadString());
            if (algorithm != SSH2Util.PublicKeyAlgorithmName(_cInfo._algorithmForHostKeyVerification))
                throw new SSHException("Protocol Error: Host Key Algorithm Mismatch");
            byte[] sigbody = re2.ReadString();
            Debug.Assert(re2.Rest == 0);

            if (_cInfo._algorithmForHostKeyVerification == PublicKeyAlgorithm.RSA)
                VerifyHostKeyByRSA(re1, sigbody, hash);
            else if (_cInfo._algorithmForHostKeyVerification == PublicKeyAlgorithm.DSA)
                VerifyHostKeyByDSS(re1, sigbody, hash);
            else
                throw new SSHException("Bad host key algorithm " + _cInfo._algorithmForHostKeyVerification);

            //ask the client whether he accepts the host key
            if (!_startedByHost && _param.KeyCheck != null && !_param.KeyCheck(_cInfo))
                return false;
            else
                return true;
        }

        private void VerifyHostKeyByRSA(SSH2DataReader pubkey, byte[] sigbody, byte[] hash)
        {
            BigInteger exp = pubkey.ReadMPInt();
            BigInteger mod = pubkey.ReadMPInt();
            Debug.Assert(pubkey.Rest == 0);

            //Debug.WriteLine(exp.ToHexString());
            //Debug.WriteLine(mod.ToHexString());

            RSAPublicKey pk = new RSAPublicKey(exp, mod);
            pk.VerifyWithSHA1(sigbody, new SHA1CryptoServiceProvider().ComputeHash(hash));
            _cInfo._hostkey = pk;
        }

        private void VerifyHostKeyByDSS(SSH2DataReader pubkey, byte[] sigbody, byte[] hash)
        {
            BigInteger p = pubkey.ReadMPInt();
            BigInteger q = pubkey.ReadMPInt();
            BigInteger g = pubkey.ReadMPInt();
            BigInteger y = pubkey.ReadMPInt();
            Debug.Assert(pubkey.Rest == 0);

            //Debug.WriteLine(p.ToHexString());
            //Debug.WriteLine(q.ToHexString());
            //Debug.WriteLine(g.ToHexString());
            //Debug.WriteLine(y.ToHexString());


            DSAPublicKey pk = new DSAPublicKey(p, g, q, y);
            pk.Verify(sigbody, new SHA1CryptoServiceProvider().ComputeHash(hash));
            _cInfo._hostkey = pk;
        }

        private byte[] DeriveKey(BigInteger key, byte[] hash, char ch, int length)
        {
            byte[] result = new byte[length];

            SSH2DataWriter wr = new SSH2DataWriter();
            wr.Write(key);
            wr.Write(hash);
            wr.Write((byte)ch);
            wr.Write(_sessionID);
            byte[] h1 = new SHA1CryptoServiceProvider().ComputeHash(wr.ToByteArray());
            if (h1.Length >= length)
            {
                Array.Copy(h1, 0, result, 0, length);
                return result;
            }
            else
            {
                wr = new SSH2DataWriter();
                wr.Write(key);
                wr.Write(_sessionID);
                wr.Write(h1);
                byte[] h2 = new SHA1CryptoServiceProvider().ComputeHash(wr.ToByteArray());
                if (h1.Length + h2.Length >= length)
                {
                    Array.Copy(h1, 0, result, 0, h1.Length);
                    Array.Copy(h2, 0, result, h1.Length, length - h1.Length);
                    return result;
                }
                else
                    throw new SSHException("necessary key length is too big"); //long key is not supported
            }
        }

        private static void CheckAlgorithmSupport(string title, string data, string algorithm_name)
        {
            string[] t = data.Split(',');
            foreach (string s in t)
            {
                if (s == algorithm_name) return; //found!
            }
            throw new SSHException("Server does not support " + algorithm_name + " for " + title);
        }
        private PublicKeyAlgorithm DecideHostKeyAlgorithm(string data)
        {
            string[] t = data.Split(',');
            foreach (PublicKeyAlgorithm a in _param.PreferableHostKeyAlgorithms)
            {
                if (SSHUtil.ContainsString(t, SSH2Util.PublicKeyAlgorithmName(a)))
                {
                    return a;
                }
            }
            throw new SSHException("The negotiation of host key verification algorithm is failed");
        }
        private CipherAlgorithm DecideCipherAlgorithm(string data)
        {
            string[] t = data.Split(',');
            foreach (CipherAlgorithm a in _param.PreferableCipherAlgorithms)
            {
                if (SSHUtil.ContainsString(t, CipherFactory.AlgorithmToSSH2Name(a)))
                {
                    return a;
                }
            }
            throw new SSHException("The negotiation of encryption algorithm is failed");
        }
        private string FormatHostKeyAlgorithmDescription()
        {
            StringBuilder b = new StringBuilder();
            if (_param.PreferableHostKeyAlgorithms.Length == 0) throw new SSHException("HostKeyAlgorithm is not set");
            b.Append(SSH2Util.PublicKeyAlgorithmName(_param.PreferableHostKeyAlgorithms[0]));
            for (int i = 1; i < _param.PreferableHostKeyAlgorithms.Length; i++)
            {
                b.Append(',');
                b.Append(SSH2Util.PublicKeyAlgorithmName(_param.PreferableHostKeyAlgorithms[i]));
            }
            return b.ToString();
        }
        private string FormatCipherAlgorithmDescription()
        {
            StringBuilder b = new StringBuilder();
            if (_param.PreferableCipherAlgorithms.Length == 0) throw new SSHException("CipherAlgorithm is not set");
            b.Append(CipherFactory.AlgorithmToSSH2Name(_param.PreferableCipherAlgorithms[0]));
            for (int i = 1; i < _param.PreferableCipherAlgorithms.Length; i++)
            {
                b.Append(',');
                b.Append(CipherFactory.AlgorithmToSSH2Name(_param.PreferableCipherAlgorithms[i]));
            }
            return b.ToString();
        }

        /*
		 * the seed of diffie-hellman KX defined in the spec of SSH2
		 */
        private static BigInteger _dh_prime = null;
        private static BigInteger DH_PRIME
        {
            get
            {
                if (_dh_prime == null)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1");
                    sb.Append("29024E088A67CC74020BBEA63B139B22514A08798E3404DD");
                    sb.Append("EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245");
                    sb.Append("E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED");
                    sb.Append("EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381");
                    sb.Append("FFFFFFFFFFFFFFFF");
                    _dh_prime = new BigInteger(sb.ToString(), 16);
                }
                return _dh_prime;
            }
        }

    }

}
